/**
 * Copyright (C) 2016-2019 DSpot Sp. z o.o
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dspot.declex.handler;

import com.dspot.declex.annotation.LocalDBModel;
import com.dspot.declex.annotation.Model;
import com.dspot.declex.annotation.UseLocalDB;
import com.dspot.declex.annotation.UseModel;
import com.dspot.declex.api.util.FormatsUtils;
import com.dspot.declex.handler.base.BaseTemplateHandler;
import com.dspot.declex.holder.UseModelHolder;
import com.dspot.declex.util.SharedRecords;
import com.dspot.declex.util.TypeUtils;
import com.helger.jcodemodel.*;

import org.atteo.evo.inflector.English;
import org.ohosannotations.ElementValidation;
import org.ohosannotations.OhosAnnotationsEnvironment;
import org.ohosannotations.helper.CanonicalNameConstants;
import org.ohosannotations.helper.ModelConstants;
import org.ohosannotations.holder.EComponentHolder;

import javax.lang.model.element.*;
import javax.lang.model.type.TypeMirror;

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.helger.jcodemodel.JExpr.*;

public class LocalDBModelHandler extends BaseTemplateHandler<EComponentHolder> {

    private Map<String, String> columnFields = new LinkedHashMap<String, String>();
    private List<String> isList = new ArrayList<String>();

    public LocalDBModelHandler(OhosAnnotationsEnvironment environment) {
        super(LocalDBModel.class, environment,
            "com/dspot/declex/template/", "LocalDBModel.ftl.java");
    }

    @Override
    public void getDependencies(Element element, Map<Element, Object> dependencies) {
        if (element.getKind().equals(ElementKind.CLASS)) {
            dependencies.put(element, UseModel.class);
        } else {
            dependencies.put(element, Model.class);
        }
    }

    @Override
    protected void setTemplateDataModel(Map<String, Object> rootDataModel,
                                        Element element, EComponentHolder holder) {
        super.setTemplateDataModel(rootDataModel, element, holder);

        rootDataModel.put("columnFields", columnFields);
        rootDataModel.put("isList", isList);

        UseModelHolder useModelHolder = holder.getPluginHolder(new UseModelHolder(holder));
        rootDataModel.put("fullInitVar", useModelHolder.getFullInitVar().name());
    }

    @Override
    public void validate(Element element, ElementValidation valid) {
        if (element.getKind().isField()) {
            Model annotated = adiHelper.getAnnotation(element, Model.class);
            if (annotated == null) {
                valid.addError("You can only apply this annotation in a field annotated with @Model");
            }

            return;
        }

        String applicationClassName = getEnvironment().getOhosManifest().getApplicationClassName();
        if (applicationClassName != null) {

            if (applicationClassName.endsWith(ModelConstants.generationSuffix())) {
                applicationClassName = applicationClassName.substring(0, applicationClassName.length() - 1);
            }

            if (!TypeUtils.isClassAnnotatedWith(applicationClassName, UseLocalDB.class, getEnvironment())) {
                valid.addError("The current application \"" + applicationClassName + "\" should be annotated with @UseLocalDB");
            }
        } else {
            valid.addError("To use LocalDBModels you should declare an application object");
        }

        validatorHelper.extendsType(element, "com.activeharmony.Model", valid);

        if (valid.isValid()) {
            TypeElement typeElement = (TypeElement) element;
            final String qualifiedName = typeElement.getQualifiedName().toString();
            SharedRecords.addDBModelGeneratedClass(qualifiedName, getEnvironment());
        }
    }


    public void getLocalDBModelFields(TypeElement element) {
        //Get @Column annotated fields
        List<? extends Element> elems = element.getEnclosedElements();
        for (Element elem : elems)
            if (elem.getKind() == ElementKind.FIELD) {
                List<? extends AnnotationMirror> annotations = elem.getAnnotationMirrors();
                for (AnnotationMirror annotation : annotations)
                    if (annotation.getAnnotationType().toString().equals("com.activeharmony.annotation.Column") ||
                        annotation.getAnnotationType().toString().equals(Model.class.getCanonicalName())) {
                        String elemName = elem.getSimpleName().toString();
                        String elemType = elem.asType().toString();

                        if (!elem.getModifiers().isEmpty() && annotation.getAnnotationType().toString().equals("com.activeharmony.annotation.Column"))
                            for (Modifier modifier : elem.getModifiers()) {
                                if (!modifier.equals(Modifier.PUBLIC) && !modifier.equals(Modifier.PROTECTED)) {
                                    LOGGER.error(
                                        "The @Column field " + elemName + " shouldn't have that modifier." +
                                            "\nRemove " + elem.getModifiers().iterator().next().name() + " modifier",
                                        element,
                                        element.getAnnotation(LocalDBModel.class)
                                    );
                                }
                            }

                        if (elem.getModifiers().contains(Modifier.STATIC)) continue;

                        //Detect when the method is a List, in order to generate all the Adapters structures
                        boolean isList = TypeUtils.isSubtype(elem, CanonicalNameConstants.LIST, getProcessingEnvironment());
                        if (isList) {
                            Matcher matcher = Pattern.compile("[a-zA-Z_][a-zA-Z_0-9.]+<([a-zA-Z_][a-zA-Z_0-9.]+)>").matcher(elemType);
                            if (matcher.find()) {
                                elemType = matcher.group(1);
                            }
                        }

                        if (elemType.endsWith(ModelConstants.generationSuffix())) {
                            String elemGeneratedType = codeModelHelper.typeStringToClassName(elemType, element);
                            TypeElement typeElement = getProcessingEnvironment().getElementUtils().getTypeElement(elemGeneratedType.substring(0, elemGeneratedType.length() - 1));
                            if (typeElement == null) continue;

                            LocalDBModel annotated = typeElement.getAnnotation(LocalDBModel.class);
                            if (annotated != null) {
                                this.columnFields.put(elemName, elemGeneratedType);
                                this.isList.add(isList ? "true" : "false");

                                break;
                            }
                        }
                    }
            }

        //Apply to Extensions
        List<? extends TypeMirror> superTypes = getProcessingEnvironment().getTypeUtils().directSupertypes(element.asType());
        for (TypeMirror type : superTypes) {
            TypeElement superElement = getProcessingEnvironment().getElementUtils().getTypeElement(type.toString());
            if (superElement == null) continue;

            if (adiHelper.hasAnnotation(superElement, UseModel.class)) {
                getLocalDBModelFields(superElement);
            }

            break;
        }
    }

    @Override
    public void process(Element element, EComponentHolder holder) {

        if (element.getKind().isField()) return;
        if (element instanceof ExecutableElement) return;

        columnFields.clear();
        isList.clear();
        getLocalDBModelFields((TypeElement) element);

        final UseModelHolder useModelHolder = holder.getPluginHolder(new UseModelHolder(holder));

        ExecutableElement dbModelLoaded = useModelHolder.getAfterLoadMethod();
        if (dbModelLoaded != null
            && dbModelLoaded.getAnnotation(LocalDBModel.class) == null) {
            dbModelLoaded = null;
        }

        ExecutableElement dbModelPut = useModelHolder.getAfterPutMethod();
        if (dbModelPut != null
            && dbModelPut.getAnnotation(LocalDBModel.class) == null) {
            dbModelPut = null;
        }

        super.process(element, holder);

        createGetLocalDBModelQueryDefault(element, holder);

        //Set the table name
        String tableName = element.getAnnotation(LocalDBModel.class).table();
        if (tableName.equals("")) {
            tableName = ((TypeElement) element).getSimpleName().toString().toLowerCase();
            if (!tableName.endsWith("s")) tableName = English.plural(tableName);
        }
        useModelHolder.getGeneratedClass().annotate(getJClass("com.activeharmony.annotation.Table")).param("name", tableName);

        insertInGetModel(dbModelLoaded, element, useModelHolder);

        insertInSelectedGetModel(dbModelLoaded, element, useModelHolder);

        insertInGetModelList(dbModelLoaded, element, useModelHolder);

        insertInSelectedGetModelList(dbModelLoaded, element, useModelHolder);

        if (!element.getAnnotation(LocalDBModel.class).ignorePut()) {
            insertInPutModel(dbModelPut, element, useModelHolder);
        }

        JMethod readObjectMethod = useModelHolder.getReadObjectMethod();
        JMethod writeObjectMethod = useModelHolder.getWriteObjectMethod();
        writeObjectMethod.body().add(invoke(ref("oos"), "writeObject").arg(_this().invoke("getId")));
        readObjectMethod.body().add(invoke(_this(), "setId")
            .arg(cast(getJClass(Long.class), ref("ois").invoke("readObject"))));
    }

    private void createGetLocalDBModelQueryDefault(Element element, EComponentHolder holder) {

        JMethod getServerModelQueryDefault =
            holder.getGeneratedClass().method(JMod.PUBLIC | JMod.STATIC, getClasses().STRING, "getLocalDBModelQueryDefault");

        LocalDBModel annotation = element.getAnnotation(LocalDBModel.class);
        if (!annotation.defaultQuery().equals("")) {
            getServerModelQueryDefault.body()._return(FormatsUtils.expressionFromString(annotation.defaultQuery()));
        } else {
            getServerModelQueryDefault.body()._return(lit(""));
        }

    }

    private void insertInSelectedGetModelList(ExecutableElement dbModelLoaded, Element element,
                                              UseModelHolder holder) {

        JFieldRef context = ref("context");
        JFieldRef args = ref("args");
        JFieldRef useModel = ref("useModel");

        //Write the getLocalDbModels in the generated getModelList() method inside the UseModel clause
        JBlock block = holder.getGetModelListUseBlock();
        block = block._if(useModel.invoke("equals").arg(getJClass(LocalDBModel.class).dotclass()))._then();

        JFieldRef localDbModels = ref("models");
        block.assign(localDbModels,
            invoke("getLocalDBModelList").arg(context).arg(args)
        );
        block._return(localDbModels);
    }

    private void insertInGetModelList(ExecutableElement dbModelLoaded, Element element,
                                      UseModelHolder holder) {

        JFieldRef context = ref("context");
        JFieldRef args = ref("args");

        //Write the getLocalDbModels in the generated getModelList() method
        JBlock block = holder.getGetModelListBlock();

        JFieldRef localDbModels = ref("models");
        block.assign(localDbModels,
            invoke("getLocalDBModelList").arg(context).arg(args)
        );
        block._if(localDbModels.ne(_null()).cand(localDbModels.invoke("isEmpty").not()))
            ._then()._return(localDbModels);
    }

    private void insertInSelectedGetModel(ExecutableElement dbModelLoaded, Element element,
                                          UseModelHolder holder) {

        JFieldRef context = ref("context");
        JFieldRef args = ref("args");
        JFieldRef useModel = ref("useModel");

        //Write the getLocalDbModel in the generated getModel() method inside the UseModel clause
        JBlock block = holder.getGetModelUseBlock();
        block = block._if(useModel.invoke("equals").arg(getJClass(LocalDBModel.class).dotclass()))._then();

        JFieldRef localDbModel = ref("model");
        block.assign(localDbModel,
            invoke("getLocalDBModel").arg(context).arg(args)
        );

        JConditional cond = block._if(localDbModel.ne(_null()));
        cond._then()._return(localDbModel);
        cond._else()._return(_new(holder.getGeneratedClass()).arg(context));

    }

    private void insertInPutModel(ExecutableElement dbModelPut, Element element,
                                  UseModelHolder holder) {

        JFieldRef args = ref("args");

        //Write the putLocalDbModel in the generated putModels() method
        JBlock block = holder.getPutModelInitBlock();

        JBlock putLocalDBModel = new JBlock();
        putLocalDBModel._if(ref("result").ne(_null()))._then()
            .assign(ref("result"), _this().invoke("putLocalDBModel").arg(args));


        SharedRecords.priorityAdd(
            block,
            putLocalDBModel,
            200
        );

    }

    private void insertInGetModel(ExecutableElement dbModelLoaded,
                                  Element element, UseModelHolder holder) {

        JFieldRef context = ref("context");
        JFieldRef args = ref("args");

        //Write the getLocalDbModel in the generated getModel() method
        JBlock block = holder.getGetModelBlock();

        JFieldRef localDbModel = ref("model");
        block.assign(localDbModel,
            invoke("getLocalDBModel").arg(context).arg(args)
        );
        block._if(localDbModel.ne(_null()))._then()._return(localDbModel);

    }
}
